TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import { z } from "zod";2import { withUser, parseBody, json, ApiError } from "@/lib/api";3import { duplicateConversation, exportConversation, shareConversation, revokeShare, revokeShareById, getShare, listShares, deleteMessage, EXPORT_FORMATS } from "@/lib/conversations/service";4import { LIMITS } from "@/lib/rate-limit";5import { APP_URL } from "@/lib/env";67export const dynamic = "force-dynamic";8type P = { id: string };910const schema = z.discriminatedUnion("action", [11 z.object({ action: z.literal("duplicate"), title: z.string().max(200).optional() }),12 z.object({ action: z.literal("branch"), messageId: z.string().max(64), title: z.string().max(200).optional() }),13 /** Prefer `GET /api/conversations/[id]/export?format=` for downloads; kept for existing callers. */14 z.object({ action: z.literal("export"), format: z.enum(EXPORT_FORMATS) }),15 /** `messageIds` → share only those messages (new link). Without it → the conversation's single full link (upsert). */16 z.object({ action: z.literal("share"), messageIds: z.array(z.string().max(64)).max(500).optional() }),17 /** Revoke every active link of the conversation, or one link when `shareId` is given. */18 z.object({ action: z.literal("unshare"), shareId: z.string().max(64).optional() }),19 z.object({ action: z.literal("share-status") }),20 z.object({ action: z.literal("list-shares") }),21 z.object({ action: z.literal("delete-message"), messageId: z.string().max(64) }),22]);2324export const POST = withUser<P>(25 async ({ req, user }, { id }) => {26 const body = await parseBody(req, schema);27 switch (body.action) {28 case "duplicate":29 return json({ conversation: await duplicateConversation(user.id, id, { title: body.title }) }, { status: 201 });30 case "branch":31 return json({ conversation: await duplicateConversation(user.id, id, { uptoMessageId: body.messageId, title: body.title }) }, { status: 201 });32 case "export": {33 const out = await exportConversation(user.id, id, body.format, { appUrl: APP_URL });34 return new Response(out.body, { headers: { "Content-Type": out.contentType, "Content-Disposition": `attachment; filename="${out.filename}"` } });35 }36 case "share": {37 const res = await shareConversation(user.id, id, { messageIds: body.messageIds });38 return json({ ...res, path: `/share/${res.id}` }, { status: res.created ? 201 : 200 });39 }40 case "unshare":41 if (body.shareId) await revokeShareById(user.id, body.shareId);42 else await revokeShare(user.id, id);43 return json({ ok: true });44 case "share-status":45 return json({ share: await getShare(user.id, id), shares: await listShares(user.id, id) });46 case "list-shares":47 return json({ shares: await listShares(user.id, id) });48 case "delete-message":49 await deleteMessage(user.id, id, body.messageId);50 return json({ ok: true });51 default:52 throw new ApiError(400, "Unknown action");53 }54 },55 { limit: { ...LIMITS.share, key: "conv-actions" } },56);57